class Archive {
  // netcdf single-file version. Replaces the text-file Archive used up through NPZmachine 2.6.
  // now saves all vars, fluxes, and params: no need to specify which to save.
  
  float dt = 1; // one save per time unit by default
  float saveIntervalTolerance = 1e-3; // helpful when the timestep sent to Environment.integrate() is comparable to dt. If the integrate() timesteps are much shorter, make this 0.
  State[] states = new State[0];
  State lastSave = null;
  String filename;
  Environment env;
  boolean announceSaves = false;
  boolean clearOnRewind = true;
  String[] infoNames = new String[0];
  Object[] infoValues = new Object[0];
  boolean saveFluxes = true;
  String[] exclusions = new String[0];
  
   
  Archive(String filename, float dt, Environment env) {
    this.filename = filename;
    this.dt = dt;
    this.env = env;
  }
  
  void save(State S) { // push a snapshot of the system into the archive
    lastSave = S;
    states = (State[])append(states,S);
    if (announceSaves) println("t = " + lastSave.getVar("t",0));
  }
  
  void offer(State S) { // let the archive decide if it wants this snapshot
    boolean saveIt = (lastSave == null);
    if (!saveIt) saveIt = (S.getVar("t",0) >= (1.-saveIntervalTolerance) * (lastSave.getVar("t",0) + dt));
    if (saveIt) save(S);
  }
  
  void rewind(State S) {
    if (clearOnRewind) {
      states = new State[0];
      lastSave = null;
      if (announceSaves) println("archive cleared");
    }
    save(S);
  }
  
  void addNameValuePair(String name, float value) {
    infoNames = append(infoNames, name);
    infoValues = (Object[])append(infoValues, new Float(value));
  }
  void addNameValuePair(String name, float[] value) {
    infoNames = append(infoNames, name);
    infoValues = (Object[])append(infoValues, value);
  }
  void addNameValuePair(String name, float[][] value) {
    infoNames = append(infoNames, name);
    infoValues = (Object[])append(infoValues, value);
  }
    
  void exclude(String name) {
    exclusions = (String[])append(exclusions, name);
  }
    
  void toFile() {
    NetcdfFileWriteable nc = null;
    nc = NetcdfFileWriteable.createNew(filename + ".nc", false);
    if (announceSaves) println("creating " + nc.getLocation());
    ucar.nc2.Dimension timeDim = nc.addDimension("time", states.length);
    ucar.nc2.Dimension cellsDim = nc.addDimension("cells", env.Ncells);
    ucar.nc2.Dimension oneDim = nc.addDimension("one", 1);
            
    defineNameValuePairs(nc, oneDim);      
    defineVars(nc, timeDim, cellsDim, oneDim);
    if (saveFluxes) defineFluxes(nc, timeDim, cellsDim, oneDim);      
    defineParams(nc, timeDim, oneDim);
    
    try {  
      nc.create(); // create the file
    } catch(IOException ioe) {
      println(ioe.toString());
    }
    
    writeNameValuePairs(nc);      
    writeVars(nc);      
    if (saveFluxes) writeFluxes(nc);      
    writeParams(nc);
      
    try {
      nc.close();
    } catch(IOException ioe) {
      println(ioe.toString());
    }

  }
  
  void defineNameValuePairs(NetcdfFileWriteable nc, ucar.nc2.Dimension oneDim) { // define scalar netcdf variables for name-value pairs
    ucar.ma2.DataType FLOAT = ucar.ma2.DataType.FLOAT;
    for (int i=0; i<infoNames.length; i++) {
      if (!stringInList(infoNames[i],exclusions)) {    
        if (infoValues[i] instanceof Float) {
          nc.addVariable(infoNames[i], FLOAT, new ucar.nc2.Dimension[] {oneDim});
        } else if (infoValues[i] instanceof float[]) {
          int L = ((float[])infoValues[i]).length;
          ucar.nc2.Dimension newDim = nc.addDimension(infoNames[i], L);
          nc.addVariable(infoNames[i], FLOAT, new ucar.nc2.Dimension[] {newDim});
        } else if (infoValues[i] instanceof float[][]) {
          float[][] v = (float[][])infoValues[i];
          int L1 = v.length;
          int L2 = v[0].length;
          ucar.nc2.Dimension newDim1 = nc.addDimension(infoNames[i] + "1", L1);
          ucar.nc2.Dimension newDim2 = nc.addDimension(infoNames[i] + "2", L2);
          nc.addVariable(infoNames[i], FLOAT, new ucar.nc2.Dimension[] {newDim1, newDim2});
        } 
        nc.addVariableAttribute(infoNames[i], "type", "info");
      }
    }
  }
  
  void defineVars(NetcdfFileWriteable nc, ucar.nc2.Dimension timeDim, ucar.nc2.Dimension cellsDim, ucar.nc2.Dimension oneDim) { // define a netcdf var for each statevar
    ucar.ma2.DataType FLOAT = ucar.ma2.DataType.FLOAT;
    for (int i=0; i<env.vars.length; i++) {
      int L = env.vars[i].length;
      String name = env.vars[i].shortname;
      if (!stringInList(name,exclusions)) {
        if (L == env.Ncells) { // [N x Ncells], like a physical variable
          nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, cellsDim});
        } else if (L == 1) { // [N x 1], like time
          nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, oneDim});        
        } else { // [N x something else]
          ucar.nc2.Dimension newDim = nc.addDimension(name, L);
          nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, newDim});
        }
        nc.addVariableAttribute(name, "type", "statevar");
        nc.addVariableAttribute(name, "fullname", env.vars[i].name);
        nc.addVariableAttribute(name, "units", env.vars[i].units.replace('µ','u'));    
      }
    }
  }

  void defineFluxes(NetcdfFileWriteable nc, ucar.nc2.Dimension timeDim, ucar.nc2.Dimension cellsDim, ucar.nc2.Dimension oneDim) { // define a netcdf var for each flux
    ucar.ma2.DataType FLOAT = ucar.ma2.DataType.FLOAT;
    for (int i=0; i<env.fluxes.length; i++) {
      int L = env.fluxes[i].length;
      String name = env.fluxes[i].shortname;
      if (!stringInList(name,exclusions)) {
        if (L == env.Ncells) {
          nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, cellsDim});
        } else if (L == 1) {
          nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, oneDim});    
        } else {
          ucar.nc2.Dimension newDim = nc.addDimension(name, L);
          nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, newDim});
        }
        nc.addVariableAttribute(name, "type", "flux");
        nc.addVariableAttribute(name, "fullname", env.fluxes[i].name);
        nc.addVariableAttribute(name, "units", env.fluxes[i].units.replace('µ','u')); 
      }
    }
  }
  
  void defineParams(NetcdfFileWriteable nc, ucar.nc2.Dimension timeDim, ucar.nc2.Dimension oneDim) { // define a netcdf var for each free param
    ucar.ma2.DataType FLOAT = ucar.ma2.DataType.FLOAT;
    for (int i=0; i<env.params.length; i++) {
      String name = env.params[i].shortname;
      if (!stringInList(name,exclusions)) {
        nc.addVariable(name, FLOAT, new ucar.nc2.Dimension[] {timeDim, oneDim});
        nc.addVariableAttribute(name, "type", "free param");
        nc.addVariableAttribute(name, "fullname", env.params[i].name);
        nc.addVariableAttribute(name, "units", env.params[i].units.replace('µ','u'));        
      }
    }
  }
  
  void writeNameValuePairs(NetcdfFileWriteable nc) { // save name-value pairs
    try {
      for (int i=0; i<infoNames.length; i++) {
        if (!stringInList(infoNames[i],exclusions)) {  
          if (infoValues[i] instanceof Float) {
            nc.write(infoNames[i], Array.factory(new float[] {(Float)infoValues[i]}));
          } else if (infoValues[i] instanceof float[]) {
            nc.write(infoNames[i], Array.factory((float[])infoValues[i]));
          } else if (infoValues[i] instanceof float[][]) {
            nc.write(infoNames[i], Array.factory((float[][])infoValues[i]));
          }
        }
      }
    } catch(IOException ioe) {
      println(ioe.toString());
    } catch(InvalidRangeException ire) {
      println(ire.toString());
    }
  }
  
  void writeVars(NetcdfFileWriteable nc) { // save each statevar to the file
    try {
      for (int i=0; i<env.vars.length; i++) {
        if (!stringInList(env.vars[i].shortname,exclusions)) {  
          for (int n=0; n<states.length; n++) {
            int K = env.vars[i].length;
            float[][] D = new float[1][K];
            for (int k=0; k<K; k++) D[0][k] = states[n].vars[i][k];
            nc.write(env.vars[i].shortname, new int[] {n,0}, Array.factory(D));
          }
        }
      }
    } catch(IOException ioe) {
      println(ioe.toString());
    } catch(InvalidRangeException ire) {
      println(ire.toString());
    }
  }
  
  void writeFluxes(NetcdfFileWriteable nc) { // save each flux to the file
    try {
      for (int i=0; i<env.fluxes.length; i++) {
        if (!stringInList(env.fluxes[i].shortname,exclusions)) {  
          for (int n=0; n<states.length; n++) {
            int K = env.fluxes[i].length;
            float[][] D = new float[1][K];
            for (int k=0; k<K; k++) D[0][k] = states[n].fluxes[i][k];
            nc.write(env.fluxes[i].shortname, new int[] {n,0}, Array.factory(D));
          }
        }
      }
    } catch(IOException ioe) {
      println(ioe.toString());
    } catch(InvalidRangeException ire) {
      println(ire.toString());
    }
  }
  
  void writeParams(NetcdfFileWriteable nc) { // save time series of each param to the file
    try {
      for (int i=0; i<env.params.length; i++) {
        if (!stringInList(env.params[i].shortname,exclusions)) {  
          for (int n=0; n<states.length; n++) {
            float[][] D = new float[1][1];
            D[0][0] = states[n].params[i];
            nc.write(env.params[i].shortname, new int[] {n,0}, Array.factory(D));
          }
        }
      }
    } catch(IOException ioe) {
      println(ioe.toString());
    } catch(InvalidRangeException ire) {
      println(ire.toString());
    }
  }
    
}

